import {
  CreateAccessDto,
  CreateAccountBalanceDto,
  CreateAccountDto,
  CreateOrderDto,
  CreateTagDto,
  CreateWatchlistItemDto,
  DeleteOwnUserDto,
  TransferBalanceDto,
  UpdateAccessDto,
  UpdateAccountDto,
  UpdateBulkMarketDataDto,
  UpdateOrderDto,
  UpdateOwnAccessTokenDto,
  UpdatePropertyDto,
  UpdateTagDto,
  UpdateUserSettingDto
} from '@ghostfolio/common/dtos';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
  Access,
  AccessTokenResponse,
  AccountBalancesResponse,
  AccountResponse,
  AccountsResponse,
  ActivitiesResponse,
  ActivityResponse,
  AiPromptResponse,
  ApiKeyResponse,
  AssetProfileIdentifier,
  AssetResponse,
  BenchmarkMarketDataDetailsResponse,
  BenchmarkResponse,
  CreateStripeCheckoutSessionResponse,
  DataProviderHealthResponse,
  DataProviderHistoricalResponse,
  ExportResponse,
  Filter,
  ImportResponse,
  InfoItem,
  LookupResponse,
  MarketDataDetailsResponse,
  MarketDataOfMarketsResponse,
  OAuthResponse,
  PortfolioDetails,
  PortfolioDividendsResponse,
  PortfolioHoldingResponse,
  PortfolioHoldingsResponse,
  PortfolioInvestmentsResponse,
  PortfolioPerformanceResponse,
  PortfolioReportResponse,
  PublicPortfolioResponse,
  SymbolItem,
  User,
  UserItem,
  WatchlistResponse
} from '@ghostfolio/common/interfaces';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
import type {
  AiPromptMode,
  DateRange,
  GroupBy
} from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n';

import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort';
import { utc } from '@date-fns/utc';
import {
  AccountBalance,
  DataSource,
  MarketData,
  Order as OrderModel,
  Tag
} from '@prisma/client';
import { format, parseISO } from 'date-fns';
import { cloneDeep, groupBy, isNumber } from 'lodash';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  public constructor(private http: HttpClient) {}

  public buildFiltersAsQueryParams({ filters }: { filters?: Filter[] }) {
    let params = new HttpParams();

    if (filters?.length > 0) {
      const {
        ACCOUNT: filtersByAccount,
        ASSET_CLASS: filtersByAssetClass,
        ASSET_SUB_CLASS: filtersByAssetSubClass,
        DATA_SOURCE: [filterByDataSource] = [],
        HOLDING_TYPE: filtersByHoldingType,
        PRESET_ID: filtersByPresetId,
        SEARCH_QUERY: filtersBySearchQuery,
        SYMBOL: [filterBySymbol] = [],
        TAG: filtersByTag
      } = groupBy(filters, (filter) => {
        return filter.type;
      });

      if (filterByDataSource) {
        params = params.append('dataSource', filterByDataSource.id);
      }

      if (filterBySymbol) {
        params = params.append('symbol', filterBySymbol.id);
      }

      if (filtersByAccount) {
        params = params.append(
          'accounts',
          filtersByAccount
            .map(({ id }) => {
              return id;
            })
            .join(',')
        );
      }

      if (filtersByAssetClass) {
        params = params.append(
          'assetClasses',
          filtersByAssetClass
            .map(({ id }) => {
              return id;
            })
            .join(',')
        );
      }

      if (filtersByAssetSubClass) {
        params = params.append(
          'assetSubClasses',
          filtersByAssetSubClass
            .map(({ id }) => {
              return id;
            })
            .join(',')
        );
      }

      if (filtersByHoldingType) {
        params = params.append('holdingType', filtersByHoldingType[0].id);
      }

      if (filtersByPresetId) {
        params = params.append('presetId', filtersByPresetId[0].id);
      }

      if (filtersBySearchQuery) {
        params = params.append('query', filtersBySearchQuery[0].id);
      }

      if (filtersByTag) {
        params = params.append(
          'tags',
          filtersByTag
            .map(({ id }) => {
              return id;
            })
            .join(',')
        );
      }
    }

    return params;
  }

  public createStripeCheckoutSession({
    couponId,
    priceId
  }: {
    couponId?: string;
    priceId: string;
  }) {
    return this.http.post<CreateStripeCheckoutSessionResponse>(
      '/api/v1/subscription/stripe/checkout-session',
      {
        couponId,
        priceId
      }
    );
  }

  public fetchAccount(aAccountId: string) {
    return this.http.get<AccountResponse>(`/api/v1/account/${aAccountId}`);
  }

  public fetchAccountBalances(aAccountId: string) {
    return this.http.get<AccountBalancesResponse>(
      `/api/v1/account/${aAccountId}/balances`
    );
  }

  public fetchAccounts({ filters }: { filters?: Filter[] } = {}) {
    const params = this.buildFiltersAsQueryParams({ filters });

    return this.http.get<AccountsResponse>('/api/v1/account', { params });
  }

  public fetchActivities({
    filters,
    range,
    skip,
    sortColumn,
    sortDirection,
    take
  }: {
    filters?: Filter[];
    range?: DateRange;
    skip?: number;
    sortColumn?: string;
    sortDirection?: SortDirection;
    take?: number;
  }): Observable<ActivitiesResponse> {
    let params = this.buildFiltersAsQueryParams({ filters });

    if (range) {
      params = params.append('range', range);
    }

    if (skip) {
      params = params.append('skip', skip);
    }

    if (sortColumn) {
      params = params.append('sortColumn', sortColumn);
    }

    if (sortDirection) {
      params = params.append('sortDirection', sortDirection);
    }

    if (take) {
      params = params.append('take', take);
    }

    return this.http.get<any>('/api/v1/order', { params }).pipe(
      map(({ activities, count }) => {
        for (const activity of activities) {
          activity.createdAt = parseISO(activity.createdAt);
          activity.date = parseISO(activity.date);
        }
        return { activities, count };
      })
    );
  }

  public fetchActivity(aActivityId: string) {
    return this.http.get<ActivityResponse>(`/api/v1/order/${aActivityId}`).pipe(
      map((activity) => {
        activity.createdAt = parseISO(activity.createdAt as unknown as string);
        activity.date = parseISO(activity.date as unknown as string);

        return activity;
      })
    );
  }

  public fetchDividends({
    filters,
    groupBy = 'month',
    range
  }: {
    filters?: Filter[];
    groupBy?: GroupBy;
    range: DateRange;
  }) {
    let params = this.buildFiltersAsQueryParams({ filters });
    params = params.append('groupBy', groupBy);
    params = params.append('range', range);

    return this.http.get<PortfolioDividendsResponse>(
      '/api/v1/portfolio/dividends',
      {
        params
      }
    );
  }

  public fetchDividendsImport({ dataSource, symbol }: AssetProfileIdentifier) {
    return this.http.get<ImportResponse>(
      `/api/v1/import/dividends/${dataSource}/${symbol}`
    );
  }

  public fetchExchangeRateForDate({
    date,
    symbol
  }: {
    date: Date;
    symbol: string;
  }) {
    return this.http.get<DataProviderHistoricalResponse>(
      `/api/v1/exchange-rate/${symbol}/${format(date, DATE_FORMAT, { in: utc })}`
    );
  }

  public deleteAccess(aId: string) {
    return this.http.delete<any>(`/api/v1/access/${aId}`);
  }

  public deleteAccount(aId: string) {
    return this.http.delete<any>(`/api/v1/account/${aId}`);
  }

  public deleteAccountBalance(aId: string) {
    return this.http.delete<any>(`/api/v1/account-balance/${aId}`);
  }

  public deleteActivities({ filters }) {
    const params = this.buildFiltersAsQueryParams({ filters });

    return this.http.delete<any>('/api/v1/order', { params });
  }

  public deleteActivity(aId: string) {
    return this.http.delete<any>(`/api/v1/order/${aId}`);
  }

  public deleteBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
    return this.http.delete<any>(`/api/v1/benchmarks/${dataSource}/${symbol}`);
  }

  public deleteOwnUser(aData: DeleteOwnUserDto) {
    return this.http.delete<any>(`/api/v1/user`, { body: aData });
  }

  public deleteTag(aId: string) {
    return this.http.delete<void>(`/api/v1/tags/${aId}`);
  }

  public deleteUser(aId: string) {
    return this.http.delete<any>(`/api/v1/user/${aId}`);
  }

  public deleteWatchlistItem({ dataSource, symbol }: AssetProfileIdentifier) {
    return this.http.delete<any>(`/api/v1/watchlist/${dataSource}/${symbol}`);
  }

  public fetchAccesses() {
    return this.http.get<Access[]>('/api/v1/access');
  }

  public fetchAsset({
    dataSource,
    symbol
  }: AssetProfileIdentifier): Observable<AssetResponse> {
    return this.http.get<any>(`/api/v1/asset/${dataSource}/${symbol}`).pipe(
      map((data) => {
        for (const item of data.marketData) {
          item.date = parseISO(item.date);
        }
        return data;
      })
    );
  }

  public fetchBenchmarkForUser({
    dataSource,
    filters,
    range,
    startDate,
    symbol,
    withExcludedAccounts
  }: {
    filters?: Filter[];
    range: DateRange;
    startDate: Date;
    withExcludedAccounts?: boolean;
  } & AssetProfileIdentifier) {
    let params = this.buildFiltersAsQueryParams({ filters });

    params = params.append('range', range);

    if (withExcludedAccounts) {
      params = params.append('withExcludedAccounts', withExcludedAccounts);
    }

    return this.http.get<BenchmarkMarketDataDetailsResponse>(
      `/api/v1/benchmarks/${dataSource}/${symbol}/${format(startDate, DATE_FORMAT, { in: utc })}`,
      { params }
    );
  }

  public fetchBenchmarks() {
    return this.http.get<BenchmarkResponse>('/api/v1/benchmarks');
  }

  public fetchDataProviderHealth(dataSource: DataSource) {
    return this.http.get<DataProviderHealthResponse>(
      `/api/v1/health/data-provider/${dataSource}`
    );
  }

  public fetchExport({
    activityIds,
    filters
  }: {
    activityIds?: string[];
    filters?: Filter[];
  } = {}) {
    let params = this.buildFiltersAsQueryParams({ filters });

    if (activityIds) {
      params = params.append('activityIds', activityIds.join(','));
    }

    return this.http.get<ExportResponse>('/api/v1/export', {
      params
    });
  }

  public fetchHoldingDetail({
    dataSource,
    symbol
  }: {
    dataSource: DataSource;
    symbol: string;
  }) {
    return this.http
      .get<PortfolioHoldingResponse>(
        `/api/v1/portfolio/holding/${dataSource}/${symbol}`
      )
      .pipe(
        map((data) => {
          if (data.activities) {
            for (const order of data.activities) {
              order.createdAt = parseISO(order.createdAt as unknown as string);
              order.date = parseISO(order.date as unknown as string);
            }
          }

          return data;
        })
      );
  }

  public fetchInfo(): InfoItem {
    const info = cloneDeep((window as any).info);
    const utmSource = window.localStorage.getItem('utm_source') as
      | 'ios'
      | 'trusted-web-activity';

    info.globalPermissions = filterGlobalPermissions(
      info.globalPermissions,
      utmSource
    );

    return info;
  }

  public fetchInvestments({
    filters,
    groupBy = 'month',
    range
  }: {
    filters?: Filter[];
    groupBy?: GroupBy;
    range: DateRange;
  }) {
    let params = this.buildFiltersAsQueryParams({ filters });
    params = params.append('groupBy', groupBy);
    params = params.append('range', range);

    return this.http.get<PortfolioInvestmentsResponse>(
      '/api/v1/portfolio/investments',
      { params }
    );
  }

  public fetchMarketDataBySymbol({
    dataSource,
    symbol
  }: {
    dataSource: DataSource;
    symbol: string;
  }): Observable<MarketDataDetailsResponse> {
    return this.http
      .get<any>(`/api/v1/market-data/${dataSource}/${symbol}`)
      .pipe(
        map((data) => {
          for (const item of data.marketData) {
            item.date = parseISO(item.date);
          }
          return data;
        })
      );
  }

  public fetchMarketDataOfMarkets({
    includeHistoricalData
  }: {
    includeHistoricalData?: number;
  }): Observable<MarketDataOfMarketsResponse> {
    let params = new HttpParams();

    if (includeHistoricalData) {
      params = params.append('includeHistoricalData', includeHistoricalData);
    }

    return this.http.get<any>('/api/v1/market-data/markets', { params }).pipe(
      map((data) => {
        for (const item of data.fearAndGreedIndex.CRYPTOCURRENCIES
          ?.historicalData ?? []) {
          item.date = parseISO(item.date);
        }

        for (const item of data.fearAndGreedIndex.STOCKS?.historicalData ??
          []) {
          item.date = parseISO(item.date);
        }

        return data;
      })
    );
  }

  public fetchPortfolioDetails({
    filters,
    withMarkets = false
  }: {
    filters?: Filter[];
    withMarkets?: boolean;
  } = {}): Observable<PortfolioDetails> {
    let params = this.buildFiltersAsQueryParams({ filters });

    if (withMarkets) {
      params = params.append('withMarkets', withMarkets);
    }

    return this.http
      .get<any>('/api/v1/portfolio/details', {
        params
      })
      .pipe(
        map((response) => {
          if (response.holdings) {
            for (const symbol of Object.keys(response.holdings)) {
              response.holdings[symbol].assetClassLabel = translate(
                response.holdings[symbol].assetClass
              );

              response.holdings[symbol].assetSubClassLabel = translate(
                response.holdings[symbol].assetSubClass
              );

              response.holdings[symbol].dateOfFirstActivity = response.holdings[
                symbol
              ].dateOfFirstActivity
                ? parseISO(response.holdings[symbol].dateOfFirstActivity)
                : undefined;

              response.holdings[symbol].value = isNumber(
                response.holdings[symbol].value
              )
                ? response.holdings[symbol].value
                : response.holdings[symbol].valueInPercentage;
            }
          }

          return response;
        })
      );
  }

  public fetchPortfolioHoldings({
    filters,
    range
  }: {
    filters?: Filter[];
    range?: DateRange;
  } = {}) {
    let params = this.buildFiltersAsQueryParams({ filters });

    if (range) {
      params = params.append('range', range);
    }

    return this.http
      .get<PortfolioHoldingsResponse>('/api/v1/portfolio/holdings', {
        params
      })
      .pipe(
        map((response) => {
          if (response.holdings) {
            for (const symbol of Object.keys(response.holdings)) {
              response.holdings[symbol].assetClassLabel = translate(
                response.holdings[symbol].assetClass
              );

              response.holdings[symbol].assetSubClassLabel = translate(
                response.holdings[symbol].assetSubClass
              );

              response.holdings[symbol].dateOfFirstActivity = response.holdings[
                symbol
              ].dateOfFirstActivity
                ? parseISO(response.holdings[symbol].dateOfFirstActivity)
                : undefined;

              response.holdings[symbol].value = isNumber(
                response.holdings[symbol].value
              )
                ? response.holdings[symbol].value
                : response.holdings[symbol].valueInPercentage;
            }
          }

          return response;
        })
      );
  }

  public fetchPortfolioPerformance({
    filters,
    range,
    withExcludedAccounts = false,
    withItems = false
  }: {
    filters?: Filter[];
    range: DateRange;
    withExcludedAccounts?: boolean;
    withItems?: boolean;
  }): Observable<PortfolioPerformanceResponse> {
    let params = this.buildFiltersAsQueryParams({ filters });
    params = params.append('range', range);

    if (withExcludedAccounts) {
      params = params.append('withExcludedAccounts', withExcludedAccounts);
    }

    if (withItems) {
      params = params.append('withItems', withItems);
    }

    return this.http
      .get<any>(`/api/v2/portfolio/performance`, {
        params
      })
      .pipe(
        map((response) => {
          if (response.firstOrderDate) {
            response.firstOrderDate = parseISO(response.firstOrderDate);
          }

          return response;
        })
      );
  }

  public fetchPortfolioReport() {
    return this.http.get<PortfolioReportResponse>('/api/v1/portfolio/report');
  }

  public fetchPrompt({
    filters,
    mode
  }: {
    filters?: Filter[];
    mode: AiPromptMode;
  }) {
    const params = this.buildFiltersAsQueryParams({ filters });

    return this.http.get<AiPromptResponse>(`/api/v1/ai/prompt/${mode}`, {
      params
    });
  }

  public fetchPublicPortfolio(aAccessId: string) {
    return this.http
      .get<PublicPortfolioResponse>(`/api/v1/public/${aAccessId}/portfolio`)
      .pipe(
        map((response) => {
          if (response.holdings) {
            for (const symbol of Object.keys(response.holdings)) {
              response.holdings[symbol].valueInBaseCurrency = isNumber(
                response.holdings[symbol].valueInBaseCurrency
              )
                ? response.holdings[symbol].valueInBaseCurrency
                : response.holdings[symbol].valueInPercentage;
            }
          }

          return response;
        })
      );
  }

  public fetchSymbolItem({
    dataSource,
    includeHistoricalData,
    symbol
  }: {
    dataSource: DataSource | string;
    includeHistoricalData?: number;
    symbol: string;
  }) {
    let params = new HttpParams();

    if (includeHistoricalData) {
      params = params.append('includeHistoricalData', includeHistoricalData);
    }

    return this.http.get<SymbolItem>(`/api/v1/symbol/${dataSource}/${symbol}`, {
      params
    });
  }

  public fetchSymbols({
    includeIndices = false,
    query
  }: {
    includeIndices?: boolean;
    query: string;
  }) {
    let params = new HttpParams().set('query', query);

    if (includeIndices) {
      params = params.append('includeIndices', includeIndices);
    }

    return this.http
      .get<LookupResponse>('/api/v1/symbol/lookup', { params })
      .pipe(
        map(({ items }) => {
          return items;
        })
      );
  }

  public fetchTags() {
    return this.http.get<Tag[]>('/api/v1/tags');
  }

  public fetchWatchlist() {
    return this.http.get<WatchlistResponse>('/api/v1/watchlist');
  }

  public loginAnonymous(accessToken: string) {
    return this.http.post<OAuthResponse>('/api/v1/auth/anonymous', {
      accessToken
    });
  }

  public postAccess(aAccess: CreateAccessDto) {
    return this.http.post<OrderModel>('/api/v1/access', aAccess);
  }

  public postAccount(aAccount: CreateAccountDto) {
    return this.http.post<OrderModel>('/api/v1/account', aAccount);
  }

  public postAccountBalance(aAccountBalance: CreateAccountBalanceDto) {
    return this.http.post<AccountBalance>(
      '/api/v1/account-balance',
      aAccountBalance
    );
  }

  public postApiKey() {
    return this.http.post<ApiKeyResponse>('/api/v1/api-keys', {});
  }

  public postBenchmark(benchmark: AssetProfileIdentifier) {
    return this.http.post('/api/v1/benchmarks', benchmark);
  }

  public postMarketData({
    dataSource,
    marketData,
    symbol
  }: {
    dataSource: DataSource;
    marketData: UpdateBulkMarketDataDto;
    symbol: string;
  }) {
    const url = `/api/v1/market-data/${dataSource}/${symbol}`;

    return this.http.post<MarketData>(url, marketData);
  }

  public postOrder(aOrder: CreateOrderDto) {
    return this.http.post<OrderModel>('/api/v1/order', aOrder);
  }

  public postTag(aTag: CreateTagDto) {
    return this.http.post<Tag>(`/api/v1/tags`, aTag);
  }

  public postUser() {
    return this.http.post<UserItem>('/api/v1/user', {});
  }

  public postWatchlistItem(watchlistItem: CreateWatchlistItemDto) {
    return this.http.post('/api/v1/watchlist', watchlistItem);
  }

  public putAccess(aAccess: UpdateAccessDto) {
    return this.http.put<Access>(`/api/v1/access/${aAccess.id}`, aAccess);
  }

  public putAccount(aAccount: UpdateAccountDto) {
    return this.http.put<UserItem>(`/api/v1/account/${aAccount.id}`, aAccount);
  }

  public putAdminSetting(key: string, aData: UpdatePropertyDto) {
    return this.http.put<void>(`/api/v1/admin/settings/${key}`, aData);
  }

  public putHoldingTags({
    dataSource,
    symbol,
    tags
  }: { tags: Tag[] } & AssetProfileIdentifier) {
    return this.http.put<void>(
      `/api/v1/portfolio/holding/${dataSource}/${symbol}/tags`,
      { tags }
    );
  }

  public putOrder(aOrder: UpdateOrderDto) {
    return this.http.put<UserItem>(`/api/v1/order/${aOrder.id}`, aOrder);
  }

  public putTag(aTag: UpdateTagDto) {
    return this.http.put<Tag>(`/api/v1/tags/${aTag.id}`, aTag);
  }

  public putUserSetting(aData: UpdateUserSettingDto) {
    return this.http.put<User>('/api/v1/user/setting', aData);
  }

  public redeemCoupon(couponCode: string) {
    return this.http.post('/api/v1/subscription/redeem-coupon', {
      couponCode
    });
  }

  public transferAccountBalance({
    accountIdFrom,
    accountIdTo,
    balance
  }: TransferBalanceDto) {
    return this.http.post('/api/v1/account/transfer-balance', {
      accountIdFrom,
      accountIdTo,
      balance
    });
  }

  public updateOwnAccessToken(aAccessToken: UpdateOwnAccessTokenDto) {
    return this.http.post<AccessTokenResponse>(
      '/api/v1/user/access-token',
      aAccessToken
    );
  }

  public updateUserAccessToken(aUserId: string) {
    return this.http.post<AccessTokenResponse>(
      `/api/v1/user/${aUserId}/access-token`,
      {}
    );
  }

  public updateInfo() {
    this.http.get<InfoItem>('/api/v1/info').subscribe((info) => {
      const utmSource = window.localStorage.getItem('utm_source') as
        | 'ios'
        | 'trusted-web-activity';

      info.globalPermissions = filterGlobalPermissions(
        info.globalPermissions,
        utmSource
      );

      (window as any).info = info;
    });
  }
}
